#!/usr/bin/perl -w

=head1 

    uniq_file.pl -- identifica archivos repetidos (por contenido) bajo un directorio
    
    Copyright (C) 2009  Pablo Le�n

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.


=head1 SINOPSIS

   uniq_files.pl [switches]
      --help         Obtener ayuda
      --dir          Directorio a analizar

=head1 DESCRIPCION


=head1 RCS

 	$Name$
	$Source$
 	$Id$

=cut

use English;
use strict 'refs';
use Getopt::Long ();
use Log::Log4perl;
use Data::Dumper;
use File::Find;
use Digest::SHA;
use File::Spec;


sub ScanFiles($);
sub ScanFilesWanted;
sub GetFiles();
sub GetFilesHash();
sub FileDigest($);


sub help () {
	my ($pack, $file, $line)=caller();
	system('perldoc -t '.$file);
	exit 1;
}


my ($program)=$0; $program=~s/^.*\/([^\/]+)$/$1/;
	
#
# Recibir parametros
#

my $optctl = {
    'dir' => '.'
   };

Getopt::Long::GetOptions($optctl,
	 '--help'
	,'dir=s'
	) || help();


if ( defined $optctl->{'help'} ) {
    help();
}

#
# Debug
#
Log::Log4perl::init('uniq_files.conf');

$logger = Log::Log4perl->get_logger('uniq_files');

# $logger->debug('this is a debug message');
# $logger->info('this is an info message');
# $logger->warn('etc');
# $logger->error('..');
# $logger->fatal('..');

$logger->debug("Debug activado");


#
# Init
#

$|=1; # $OUTPUT_AUTOFLUSH=1

my $sDir=$optctl->{'dir'};
die "El directorio [$sDir] no es valido."
   unless (-d $sDir);
$logger->debug(sprintf("sDir:[%s]", $sDir));

   
my ($rc);

# while(<>)
# {
# 	print;
# }

ScanFiles($sDir);

my $sFileSpec;
my $rFiles=GetFiles();

my $rFile;
# foreach $rFile (@{$rFiles})
# {
#    print sprintf("spec: %s sz:%d hash:%s\n", $rFile->[0], , $rFile->[1], (defined($rFile->[2])?$rFile->[2]:'<undef>') );
# }

my $rFilesHash=GetFilesHash();
$::logger->debug(sprintf("FilesHash: %s ", Dumper($rFilesHash)) );

my $nSize;
my $nFile;
my $sDigest;
foreach $nSize (sort {$a <=> $b} (keys %{$rFilesHash}) )
{
   next if (exists($rFilesHash->{$nSize}->{'first'}));
   
   foreach $sDigest (sort keys %{$rFilesHash->{$nSize}})
   {
      if ( scalar(@{$rFilesHash->{$nSize}->{$sDigest}}) > 1 )
      {
         print "=== Size: $nSize hash: $sDigest\n";
         foreach $nFile (@{$rFilesHash->{$nSize}->{$sDigest}})
         {
            $rFile=$rFiles->[$nFile];
            print sprintf("\t %s\n", $rFile->[0] );
            # print sprintf("spec: %s sz:%d hash:%s\n", $rFile->[0], , $rFile->[1], (defined($rFile->[2])?$rFile->[2]:'<undef>') );
         }
      }
   }
}




exit;
###############################################################################

sub Funcion
{
	my ($sParam1, $sParam2)=@_;
	my $ret='';
	
   $::logger->debug(sprintf("Funcion( %s )", join(' ', map( (defined($_)?"[$_]":'<undef>'), @_))));

	# Cuerpo de Funcion
	
   $::logger->debug(sprintf("Funcion()->[%s]", $ret));
	return $ret;
}

sub ScanFiles($)
{
	my ($sScanPath)=@_;
	my $ret='';

   $::logger->debug(sprintf("ScanFiles( %s )", join(' ', map( (defined($_)?"[$_]":'<undef>'), @_))));

	# Cuerpo de Funcion
   find(\&ScanFilesWanted, $sScanPath);

   $::logger->debug(sprintf("ScanFiles()->[%s]", $ret));
	return $ret;
}


{
my @aFiles=();
my %hFilesHash=();

sub ScanFilesWanted
{
	my ($dummy)=@_;
	my $ret='';

   $::logger->debug(sprintf("ScanFilesWanted( %s )"
      ,join(' ', map( (defined($_)?"[$_]":'<undef>')
         , ($_, $File::Find::dir, $File::Find::name) ))));

	# $File::Find::dir is the current directory name,
	# $_ is the current filename within that directory
	# $File::Find::name is the complete pathname to the file.

	# Cuerpo de Funcion

   if ( (-f $_) and (! -l $_))
   {
      #  0 dev      device number of filesystem
      #  1 ino      inode number
      #  2 mode     file mode  (type and permissions)
      #  3 nlink    number of (hard) links to the file
      #  4 uid      numeric user ID of file's owner
      #  5 gid      numeric group ID of file's owner
      #  6 rdev     the device identifier (special files only)
      #  7 size     total size of file, in bytes
      #  8 atime    last access time in seconds since the epoch
      #  9 mtime    last modify time in seconds since the epoch
      # 10 ctime    inode change time in seconds since the epoch (*)
      # 11 blksize  preferred block size for file system I/O
      # 12 blocks   actual number of blocks allocated

      my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
          $atime,$mtime,$ctime,$blksize,$blocks) = stat($_);

      if ($size > 0)
      {
         my $sFileSpec = File::Spec->rel2abs($_);
         $::logger->debug(sprintf("sFileSpec:[%s]", $sFileSpec));
         
         my @aAuxFile=($sFileSpec, $size, undef() );
         push @aFiles, \@aAuxFile;

         my $nThisIdx=scalar(@aFiles)-1;

         unless (exists($hFilesHash{$size}))
         {
            $hFilesHash{$size}->{'first'}= $nThisIdx;
         } else
         {
            if (exists($hFilesHash{$size}->{'first'}))
            {
               my $nFirst=$hFilesHash{$size}->{'first'};
               my @aAuxFiles= ( $nFirst );

               delete($hFilesHash{$size}->{'first'});
               
               my $rFirst=$aFiles[$nFirst];
               my $sFirstDigest=FileDigest($rFirst->[0]);
               $rFirst->[2]=$sFirstDigest;

               $hFilesHash{$size}->{$sFirstDigest}=\@aAuxFiles;
            }

            my $rThis=$aFiles[$nThisIdx];
            my $sDigest=FileDigest($rThis->[0]);
            $rThis->[2]=$sDigest;

            if (exists($hFilesHash{$size}->{$sDigest}))
            {
               push @{$hFilesHash{$size}->{$sDigest}}, $nThisIdx;
            } else
            {
               my @aAuxFiles= ( $nThisIdx );
               $hFilesHash{$size}->{$sDigest}=\@aAuxFiles;
            }
         }
      }
   }
   $::logger->debug(sprintf("ScanFilesWanted()->[%s]", $ret));
	return $ret;
}

sub GetFiles()
{
	my ($dummy)=@_;

   # $::logger->debug(sprintf("GetFileList( %s )", join(' ', map( (defined($_)?"[$_]":'<undef>'), @_))));

	# $File::Find::dir is the current directory name,
	# $_ is the current filename within that directory
	# $File::Find::name is the complete pathname to the file.

	# Cuerpo de Funcion
	my $ret=\@aFiles;

   # $::logger->debug(sprintf("GetFileList()->[%s]", $ret));
	return $ret;
}

sub GetFilesHash()
{
	my ($dummy)=@_;

   # $::logger->debug(sprintf("GetFileList( %s )", join(' ', map( (defined($_)?"[$_]":'<undef>'), @_))));

	# Cuerpo de Funcion
	my $ret=\%hFilesHash;

   # $::logger->debug(sprintf("GetFileList()->[%s]", $ret));
	return $ret;
}

}

sub FileDigest($)
{
	my ($sFileSpec)=@_;

   $::logger->debug(sprintf("FileDigest( %s )", join(' ', map( (defined($_)?"[$_]":'<undef>'), @_))));

	# Cuerpo de Funcion
   my $sha = Digest::SHA->new(1);

   # $sFileSpec="x.txt";
   # 
   # warn "No es archivo [$sFileSpec]"
   #    unless(-f $sFileSpec);
   # warn "No se puede abrir archivo para lectura [$sFileSpec]"
   #    unless(-r $sFileSpec);
   # 
   # open(FILE, "<:encoding(UTF-8)", $sFileSpec)
   
   open(FILE, "<", $sFileSpec)
      or die "No se pudo abrir archivo para lectura [$sFileSpec]";
   $sha->addfile(*FILE);
   close FILE;
   
   # $sha->addfile($sFileSpec, 'b');

   $ret = $sha->hexdigest;         # compute digest

   $::logger->debug(sprintf("FileDigest()->[%s]", $ret));
	return $ret;
}



